﻿/*
  BIT-1  数据在内存中的存储：
    1. 数据类型介绍
	   内置类型：int（4B）、float（4B）、double（8B）、char（1B）、short（2B）、long（4\8B）等
	   类型划分：
		 整型: %d:打印有符号数，%u:打印无符号数
		   char（char本质是ASCii码，它有三个类型（char、unsigned char、signed char）,一般取决于编译器，默认标准未定义）；
		   有符号的char类型范围是-128到127，unsigned char范围是0到255
				 unsigned char
				 signed char
		   short
				 unsigned short [int]
				 signed short [int]
		   int 
				unsigned int
				 signed int(默认)
		   long
				 unsigned long [int]
				 signed long [int]
		   long long
				   unsigned long long [int]
				   signed long long [int]
		 浮点型：
		   float
		   double

		构造类型（自定义类型）：
		   数组类型 int[5]
		   结构体类型 struct
		   枚举类型 enum
		   联合类型 union

		指针类型：
		   指针类型 int*、char*、float*、double*、void*
		   函数指针类型 int (*)(int, int)

	2.整形在内存中的存储：在<limits.h>
	   整型数据在内存中是以二进制补码的形式存储的，二进制表示形式：原码、反码、补码
	   例如：int a = -5; 在内存中存储为 11111111 11111111 11111111 11111011（32位系统）。
	   大端存储：把数据的高位存储在低地址处，低位存储在高地址处。针对字节的顺序
	   小端存储：把数据的低位存储在低地址处，高位存储在高地址处。针对字节的顺序，X86架构采用小端存储方式。由硬件决定。
	   如：int a=20;//0x00 00 00 14——大端：低——高：00 00 00 14
								   ——小端：低——高：14 00 00 00
	   设计程序来判断大小端：
	   用四个字节的第一个字节的内容进行判断
	   int a = 1;
		if (*(char*)& a == 1) {
			printf("小端\n");
		}
		else
		{
			printf("大端\n");
		}
		 ；
		                                                                        for(int i=0;i<100;i++)
																				{
																				       a[i]=-1-i;
																				}
																				//本身：-1 -2 -3 .... -1000
																				//但是有符号的char类型只能存储-128到127之间的数，所以会截断，导致-1 -2 -3...-128 127 126...3 2 1 0 -1 -2...
		但是取回不会

	3.浮点数在内存中的存储：在<float.h>，常见的浮点数：3.14f、1E10
	   浮点数在内存中是以IEEE 754标准的形式存储的，分为单精度（float）和双精度（double）。
	   整数与浮点数的存储和读取的方式不同，
	   3.1浮点数存储规则
	   整数是直接存储其二进制补码，而浮点数则需要进行转换。
		      任何一个二进制浮点数V可表示：         V=(-1)^S* M * 2^E
                         浮点数的存储包括            符号位、指数位和尾数位。
	   无法精确保存小数部分的二进制表示，因此会有精度损失。
	   单精度浮点数占4字节，双精度浮点数占8字节。单精度浮点数的存储格式为：1位符号位、8位指数位、23位尾数位。
	   例如：float a = 3.14; 在内存中存储为 01000000 10010011 00100100 11001100（32位系统）。
	  
	BIT-2 指针
	1.字符指针 char*
	字符指针是指向字符类型数据的指针，可以用来操作字符串。
		例如：char* str = "Hello"; 这里str是一个字符指针，指向字符串"Hello"的首地址.并不是存储，因此“Hello”是常量字符串，只读不可修改；输出的时候会随着'\0'结束。
				 char str1[] = "hello bit.";
				 char str2[] = "hello bit.";

				 const char *str3 = "hello bit.";
				 const char *str4 = "hello bit.";

				 if(str1 ==str2)
				 printf("str1 and str2 are same\n");
				 else
				 printf("str1 and str2 are not same\n");
				 }
				 //str1 and str2 are same  指向同一个地址

				 if(str3 ==str4)
				 printf("str3 and str4 are same\n");
				 else
				 printf("str3 and str4 are not same\n");
				 //str3 and str4 are not same
	2.指针数组
	用来存放指针的数组，指针数组可以用来存放多个字符串的首地址。
	例如：		char* arr[] = {"Hello", "World"}; 这里arr是一个指针数组，存放了两个字符串的首地址。
				char* arr[] = {"Hello", "World"};
				printf("%s\n", arr[0]); // 输出Hello
				printf("%s\n", arr[1]); // 输出World
	[]下标识符表示数组，指针数组的每个元素都是一个指针，可以通过下标访问每个字符串的首地址，表示解引用。
	
	3.数组指针
	指向数组的指针，数组指针可以用来操作整个数组。
		例如：
		        int* arr1[10];//指针数组
				int (*arr)[5] = {1, 2, 3, 4, 5}; // p是一个数组指针，指向arr数组，每个元素都是int类型
	数组名：&数组名，表示整个数组的地址，数组名是一个常量指针，指向数组的首地址，相当于&数组名[0]。
			&数组名+1表示整个数组的地址加上一个数组的大小，指向下一个数组的首地址。
			数组名+1和&数组名[0]+1表示数组名的地址加上一个元素的大小，指向下一个元素的首地址。
	int arr[5] = {1, 2, 3, 4, 5};
	int (*p2)[10]=&arr;//不能省略[]:int (*p2)[]=&arr;  error

	int sz=sizeof(arr)/sizeof(arr[0]);
	for(int i=0;i<sz;i++){
		printf("%d ",*(*p2+i));
	}
	常见用法：常用在二维数组中
	 void print_arr1(int arr[3][5], int row, int col)
	 {
		 int i = 0;
		 for(i=0; i<row; i++)
		{
				for(j=0; j<col; j++)
				{
					printf("%d ", arr[i][j]);
				}
				printf("\n");
			}
	 }
	 void print_arr2(int (*arr)[5], int row, int col)
	 {
		int i = 0;
		 for(i=0; i<row; i++)
		{
			for(j=0; j<col; j++)
			{
				 printf("%d ", *(*(arr+i)+j);
			}
			 printf("\n");
		}
	 }
	
	int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
	 print_arr1(arr, 3, 5);
	 //数组名arr，表示首元素的地址
	//但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr，其实相当于第一行的地址，是一维数组的地址
	//可以数组指针来接收
	print_arr2(arr, 3, 5);

	int(*p[10])[5];是一个存放数组指针的数组，p[0]是一个指向5个int类型元素的数组的指针。
	4.数组参数、指针参数
	4.1 一维数组参数
	int *p[20]={0};——可以由参数int *p[20]和int **p表示（二级指针可以存放一级指针的地址）
	4.2 二维数组参数
	可以由int (*arr)[5]、int arr[][5]、int arr[3][5]等表示
	4.3 一级指针参数
	如果函数参数是一个指针类型，可以传的参数有：arr、&a、p等
	4.4 二级指针参数
	如果函数参数是一个指针类型，可以传的参数有：arr(指针数组)、&p、pp、*ppp等
    
	5.函数指针
	函数指针是指向函数的指针，可以用来调用函数。
	//	例如：
		   int add(int a, int b) {...}
		   int main(){
		     printf("%d",add);
		     printf("%d",&add);
		   }
		   //		输出：地址结果一样


	       int add(int a, int b) {
				return a + b;
			}
			int main() {
				int (*func_ptr)(int, int) = &add; // 函数指针，这里func_ptr是一个函数指针，指向函数add的地址
				int  result = func_ptr(2, 3); // 调用函数
				int result = (*func_ptr)(2, 3); // 调用函数
				printf("Result: %d\n", result); // 输出结果
				return 0;
			}
	(*(void(*)())0)()://1.把0强制转换为函数指针类型，2.调用函数指针，3.函数指针指向的函数没有参数和返回值，所以不需要传递参数和返回值。
	void (*signal(int , void(*)(int)))(int);//1.函数指针类型，2.函数指针指向的函数有两个参数，第一个参数是int类型，第二个参数是函数指针类型，3.函数指针指向的函数返回值是void类型。
	用途：做一个计算器
	    int add(int a, int b)
		 {
			 return a + b;
		 }
		 int sub(int a, int b)
		 {
			 return a - b;
		 }
		 int mul(int a, int b)
		 {
			 return a*b;
		 }
		 int div(int a, int b)
		 {
			 return a / b;
		 }
		 void menu(){
			 printf( "*************************\n" );
			 printf( "  1:add           2:sub  \n" );
		   	printf( "  3:mul           4:div  \n" );
			 printf( "*************************\n" );
		 }
		 void calc(int(* p)(int int)){  //回调函数
			  int x, y,ret;
			  printf( "请输入两个数：" );
			  scanf( "%d%d", &x, &y );
			  ret = p(x, y)
			  printf("%d\n",ret);
		 }
		 int main(){
			 int x, y;
			 int input = 1;
			 int ret = 0;
			 do{
				menu();
				printf( "请选择：" );
				scanf( "%d", &input);
				swithc(input){
				case 1:
				      calc(add);
				      break;
				case 2:
					  calc(sub);
					  break;
				case 3:
					  calc(mul);
					  break;
				case 4:
					  calc(div);
					  break;
				default:
					  printf( "输入有误\n" );
					  break;
				}
			 }while(input)
			 return 0;
		 }
 
	6.函数指针数组
	将函数指针存放在数组中，可以用来存放多个函数的地址。
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; 
	  做一个计算器（优化）
	     int x, y;
		 int input = 1;
		 int ret = 0;
		 int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
		 while (input)
			 {
				 printf( "*************************\n" );
				 printf( "  1:add           2:sub  \n" );
				 printf( "  3:mul           4:div  \n" );
				 printf( "*************************\n" );
				 printf( "请选择：" );
				 scanf( "%d", &input);
				 if ((input <= 4 && input >= 1))
				  {
						 printf( "输入操作数：" );
						 scanf( "%d %d", &x, &y);
						 ret = (*p[input])(x, y);
				  }
				 else
						 printf( "输入有误\n" );
				 printf( "ret = %d\n", ret);
			  }
		 return 0;
	7.指向函数指针数组的指针
	int(*p[])(int , int ) = { 0, add, sub, mul, div };
	int(*(*p1[5]))(int , int ) = &p;
	8.回调函数
	一个通过函数指针调用的函数，可以在被调用的函数中调用回调函数。
	
	冒泡排序优化：
	qsort函数：快速排序函数，C标准库提供的排序函数，可以用来对数组进行排序，可以排序任意类型的数据，默认升序
	void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
	qsort(起始位置, 个数, 待排序的元素大小（字节）, compare函数指针int *compare(const void* p1.const void* p2)); 
	compare是一个比较函数，用来比较两个元素的大小：比较逻辑：•	返回值 < 0：第一个元素排在前面 •	返回值 = 0：顺序不变 •	返回值 > 0：第一个元素排在后面
	void*指针：无具体类型的指针，无法进行解引用操作，只能进行地址运算和强制类型转换。void*指针可以指向任意类型的数据，但无法直接访问数据，若要访问数据，需要将void*指针强制转换为具体类型的指针。
	排序整形
	#include<stdlib.h>
	int compare(const void* e1, const void* e2) {
			return (*(int*)e1) - (*(int*)e2);
	}
	int main() {
			int arr[] = { 9.8,7,6,5,4,3,2,1,0 };
			int sz = sizeof(arr) / sizeof(arr[0]);
			qsort(arr, sz, sizeof(arr[0]), compare);
			for (int i = 0; i < sz; i++) {
				printf("%d ", arr[i]);
			}
			return 0;
			}
     排序结构体：
		 struct Stu {
			char name[20];
			int age;
		}S;
		int compare(const void* e1, const void* e2) {
			return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
		}

		int main() {
			struct Stu S[] = { {"zhanghsan",20},{"hahh",18},{"jdaliw",10} };
			int sz = sizeof(S) / sizeof(S[0]);
			qsort(S, sz, sizeof(S), compare);
			for (int i = 0; i < sz; i++) {
				printf("%s", S[i].name);
				printf("%d\n", S[i].age);
			}
			return 0;
		}

	冒泡排序优化：实现qsort函数
		int compare(void* e1, void* e2) {
			return (*(int*)e1) - (*(int*)e2);
		}
		void Swap(char* x, char* y,int sz) {
			for (int i = 0; i < sz; i++) {
				char tmp = (*x);
				(*x) = (*y);
				(*y) = tmp;
				x++,y++;
			}
		}

		void bubble_sort(void* base, int sz,int width,int (*compare)(void *e1,void* e2)) {
			for (int i = 0; i < sz-1; i++) {
				int flag = 1;
				for (int j = 0; j < sz -1 - i; j++) {
					  if(compare((char*)base + j*width, (char*)base + (j+1)*width) > 0){
						  Swap((char*)base + j*width, (char*)base + (j+1)*width, width);
						  flag = 0;
					}
				}
				if (flag == 1)
					break;
			}
		}

		void test() {
			int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
			int sz = sizeof(arr) / sizeof(arr[0]);
			bubble_sort(arr, sz,sizeof(arr[0]),compare);//只考虑升序
			for (int i = 0; i < sz; i++) {
				printf("%d", arr[i]);
			}
		}
		int main() {
			test();
			return 0;
		}
	BIT-3 字符和字符串函数、内存函数
	    补充：字符转换为整形：(*str)-'0'
	    1.字符和字符串函数
		  1.1 strlen函数：	长度不受限制的函数
		      专门来计算字符串的长度，不包括'\0'结束符。 
			  如果参数无'\0'，则会导致内存越界访问，可能会导致程序崩溃，故随机值
			  返回值为size_t类型，表示字符串的长度（无符号整形）。  
			  if(stlren("abc")-strlen("aysdad")>0)  //逻辑为真

		   1.2 strcpy函数：	长度不受限制的函数
			  专门来复制字符串，复制到目标字符串中，包含'\0'结束符。
			  strcpy(目标数据，源数据)；拷贝会把'\0'也拷贝过去。
			  如果目标字符串空间不足或者源数据无'\0'或者目标数据是常量字符串(char *p="adsada")，可能会导致内存越界访问，可能会导致程序崩溃，故随机值
			  返回值为目的字符串的首地址,函数返回类型（char*）


		   1.3 strcat函数：	长度不受限制的函数
		      专门来连接字符串，连接到目标字符串的末尾，包含'\0'结束符。
			  strcat(目标数据。追加数据);
			  注意同strcpy,还有不能自己连接到自己。
			  返回值为目标字符串的首地址，函数返回类型（char*）。

		   1.4 strcmp函数：	长度不受限制的函数
			  专门来比较两个字符串的大小，比较的是ASCII码值。
			  strcmp(字符串1，字符串2)；比较两个字符串的大小，返回值为int类型。
			  如果字符串1大于字符串2，返回值大于0；如果字符串1小于字符串2，返回值小于0；如果字符串1等于字符串2，返回值等于0。
			  if(str1==str2)//判断不对，比较的是两数组的首地址，必然不对。

				  长度受限制的函数：strncpy,strncat, strncmp
					  strncpy(目标数据，源数据，长度)；拷贝指定长度的字符串到目标字符串中，包含'\0'结束符。
					  strncat(目标数据，追加数据，长度)；连接指定长度的字符串到目标字符串的末尾，包含'\0'结束符。
					  strncmp(字符串1，字符串2，长度)；比较指定长度的两个字符串的大小，返回值为int类型。

		   1.5 strstr函数：
			   专门来查找字符串中是否包含某个子字符串，返回值为指向该子字符串的指针(char *)，如果没有找到，返回NULL。
			   strstr(字符串，子字符串)；查找字符串中是否包含某个子字符串，返回值为指向该子字符串的指针，如果没有找到，返回NULL。	
			   模拟实现：
			    char* my_strstr(const char* str1,const char* str2) { //加 const修饰符，表示参数是只读的，目的是保护原字符串不被修改
						assert(str1 && str2);
						const char* s1 = str1; // 指向str1的指针,const修饰，使之能回到起点位置
						const char* s2 = str2; // 指向str2的指针
						const char* p = str1; // 用于遍历str1的指针

						while (*p) {//*p=='\0'即证明s1跑完，又证明循环结束
							s1 = p;
							s2 = str2;
							while (*s2 != '\0' && *s1!='\0'&&*s1 == *s2) {
								s1++;
								s2++;
							}
							if (*s2 == '\0')
								return p;
							p++;

						}
						return NULL;
				}
			int main() {
				char str1[] = "Hello World";
				char str2[] = "World";
				char* ret = my_strstr(str1, str2); // 查找str1中是否包含str2
				if (ret == NULL) {
					printf("子字符串未找到\n");
				}
				else {
					printf("子字符串找到，位置：%ld\n", ret - str1); // 计算子字符串在str1中的位置
					printf("子字符串内容：%s\n", ret); // 输出子字符串内容
				}
				return 0;
			}

		  1.6 strtok函数：
		  	  专门来分割字符串，分割的依据是指定的分隔符，返回值为指向分割后的字符串的指针(char *)，即字串起始位置。
			  strtok(字符串，分隔符集合)；const char* sep="@.";分割字符串，返回值为指向分割后的字符串的指针，如果没有找到分隔符，返回NULL。
			  注意：strtok函数会修改原字符串，将分隔符替换为'\0'结束符，所以原字符串会被改变。
			  strtok函数是一个状态机函数，每次调用都会返回下一个分割后的字符串，直到没有更多的字符串可供分割。
			  当第一次的参数不为NULL使，会对此数据进行临时存储
			  当第一个参数为NULL时，表示继续分割上一次的字符串，直到没有更多的字符串可供分割。
				  const char *seq="@.";
				  char str[] = "shasjdh@12313231.com";
				  char *ret=1strtok(str, seq); // 第一次调用，分割字符串shasjdh
				  ret=strtok(NULL, seq); // 第二次调用，继续分割字符串12313231
				  ret=strtok(NULL, seq); // 第三次调用，继续分割字符串com

				  //使用优化
				  for(char *ret =strtok(str,sep);ret!=NULL;ret=strtok(NULL,sep)){
					   ptintf("%s\n",ret);
				  }

		  1.7 strerror函数：
			  专门来获取错误信息，返回值为指向错误信息的指针(char *)。
			  strerror(int 错误码)；获取错误码对应的错误信息，返回值为指向错误信息的指针，如果错误码无效，返回NULL。
			  注意：strerror函数会根据当前的错误码返回对应的错误信息，所以每次调用都会返回不同的错误信息。
			  错误码可以通过errno变量获取，errno变量是一个全局变量，用来表示当前的错误码。
			  errno 参数// 设置错误码为无效参数,C语言标准库中定义的全局错误码
			  FILE* pf= fopen("test.txt", "r");
			  if(pf==NULL){
			    printf("%s",strerror(errno));
			  }
		  1.8 字符分类函数：
			  isalpha()：判断字符是否是字母
			  isdigit()：判断字符是否是数字
			  isspace()：判断字符是否是空白字符（空格、制表符、换行符等）
			  isupper()：判断字符是否是大写字母
			  islower()：判断字符是否是小写字母
			  isalnum()：判断字符是否是字母或数字
			  isprint()：判断字符是否是可打印字符
			  iscntrl()：判断字符是否是控制字符
			  ispunct()：判断字符是否是标点符号
		   1.9 字符转换函数：<ctype.h>
			   toupper()：将字符转换为大写字母
			   tolower()：将字符转换为小写字母
			   isxdigit()：判断字符是否是十六进制数字
			   isgraph()：判断字符是否是可打印字符（不包括空格）
			   isblank()：判断字符是否是空白字符（空格或制表符）
			   isascii()：判断字符是否是ASCII字符
			   isleadbyte()：判断字符是否是双字节字符的首字节
			   istrailbyte()：判断字符是否是双字节字符的尾字节
			   atoi函数：将字符串转换为整数，返回值为int类型 //<stdlib.h>  非数字字符会被忽略，直到遇到第一个非数字字符为止。
			 

		  2.0 memcpy函数
			  内存拷贝函数，专门来拷贝内存中的数据，拷贝指定长度的内存数据。返回值为指向目标地址的指针(void *)，即首地址。
			  memcpy(void*目标地址，const void*源地址，长度)；拷贝指定长度的内存数据到目标地址中。
			  不知道其类型的情况下，可以使用memcpy函数来拷贝内存中的数据。同理自己拷贝自己不行（要用memove ）
		  2.1 memmove 函数
		      memcpy(void*目标地址，const void*源地址，长度)；允许重叠变量进行复制
		  2.2 memcmp 函数
			   memcpy(void*目标地址，const void*源地址，长度)；内存数值比较函数，专门来比较内存中的数据，比较指定长度的内存数据，返回值为int类型。
		  2.3 memset 函数
	    		  memset(void*目标地址，值，长度)；专门来设置内存中的数据，设置指定长度的内存数据为指定的值。

       BIT-3 自定义类型：结构体、枚举、联合体
	      1.结构体：
			struct Student {
				//数据相关属性
				char name[20];
				int age;
				float score;
			}创建结构体变量-list（全局变量）;
			或
			struct Student p;
			
			特殊声明：
			匿名结构体类型：
			struct {
				char name[20];
				int age;
				float score;};
		//匿名结构体类型可以直接使用，不需要再定义变量名，只能用一次；
		    
			结构的自引用：
			链表：
			struct Node {
				int data;
				struct Node* next; // 指向下一个节点的指针
			};

			结构体的初始化：
			struct Student {
				//数据相关属性
				char name[20];
				int age;
				float score;
			}p={"xiaoming",12,98.0};
			或
			struct Student p={"xiaoming",12,98.0};
		1.2 结构体的内存对齐：
			结构体的内存对齐是为了提高访问效率，编译器会根据结构体成员的类型和大小进行内存对齐。
			对齐规则：1.第一个成员在与结构体变量偏移量为0的地址处
					  2.其他成语变量要对齐到某个数字（对齐数）的整数倍的地址处（参考偏移量：对齐数是多少，偏移量也就是存储位置就在其对齐数的整数倍的位置）
					    对齐数=编译器默认 的一个对齐数与改成员大小的较小值（vs默认值为8）
					  3.结构体总大小为最大对齐数（每个成员变量都有一个对齐数）的整数倍，如果不为整数倍，则会在结构体末尾添加填充字节（padding bytes）来满足对齐要求。
					  4.如果嵌套了结构体的情况，嵌套的结构体对齐到自己的最大对齐数的整数倍，结构体的整体大小就是所有最大对齐数（含嵌套结构体的对齐数）的整数倍
			意义：用空间来换时间，减少内存访问次数，提高访问效率。
		2. 位段
		与结构体类似，但是位段是用来节省内存空间的，可以指定每个成员变量的位数。跨平台性能不行
		struct A{
		  int A:2; // A占用2位(bit)
		  int B:3; // B占用3位
		}；
		位段的内存分配：以4个字节（int）或1个字节（char）为单位进行分配，位段的大小必须是字节的整数倍。
		成员变量的位数不能超过对于类型字节的位数（8位或32位），否则会导致编译错误。
		应用于网络编程
		3.枚举：
		创建出了枚举常量，表示一组相关的常量，不会在内存中占用额外的空间，枚举常量的值从0开始递增。
		enum Color {
			RED,    // 0
			GREEN,  // 1
			BLUE    // 2
		};
		enum Color color = RED; // 使用枚举常量，这时才占用内存空间
		4.联合体：
		联合体是一种特殊的结构体，所有成员共享同一块内存空间，联合体的大小等于最大成员的大小。
		union Data {
			int i;
			float f;
			char c;
		};
		4.1 联合体的计算：
			也存在对齐规则，联合体的大小是最大成员的大小的整数倍（只是整数倍）。
		union un{
		   char arr[5];//1*5
		   int i;//4*1
		};
		sizeof(un);//8 
	  BIT-4 动态内存管理
	    int a=10;//静态申请固定的空间，无法进行空间更改
		malloc、realloc、calloc、free函数可以进行自己管理空间大小，申请的内存会一直在程序中，直到程序结束或者遇到free函数

		1.malloc函数
		  void* malloc(size_t size);
		  malloc函数用于动态分配内存，返回值为void*类型，表示分配的内存地址，若分配失败，返回NULL。
		2.free函数
		  void free(void* ptr);
		  free函数用于释放动态分配的内存，释放后指针不再指向有效内存，若指针为NULL，则不做任何操作。
		  通常与malloc函数配合使用，释放malloc函数分配的内存，程序关闭自动回收内存。
		    int main() {
			int arr[10] = { 0 };
			int* p=(int *)malloc(40);
			//前二者申请空间的差别是，arr申请在了栈区，而p申请在了堆区，栈区是静态分配的内存，堆区是动态分配的内存。
			if (p == NULL) {
				printf("%s\n", strerror(errno));// 判断malloc申请空间是否成功，strerror函数获取错误信息
				return 1; // 申请失败，返回错误码
			}
			for (int i = 0; i < 10; i++) {
				*(p+i) = i ;
			}
			for (int i = 0; i < 10; i++) {
				printf("%d ", *(p + i));
			}

			free(p);
			p = NULL;//释放了p的内存空间，但是p本身还指向原来的地址，所哟为防止p成为野指针，应该将p置为NULL。
			return 0;
		}
	   3.calloc函数
		 void* calloc(size_t num, size_t size);
		 calloc函数用于动态分配内存，返回值为void*类型，表示分配的内存地址，若分配失败，返回NULL。
		 calloc函数与malloc函数的区别在于，calloc函数会将分配的内存初始化为0，而malloc函数不会。

	   4.realloc函数
		 void* realloc(void* ptr, size_t size);		ptr是指向要调整内存块的指针，size是新的内存块大小。 size:原+新
		 realloc函数用于重新分配内存，返回值为void*类型，表示重新分配的内存地址，若分配失败，返回NULL。
		 realloc函数可以调整已经分配的内存块的大小，如果新的大小大于原来的大小，则会在原来的内存块后面追加新的内存块；如果新的大小小于原来的大小，则会将原来的内存块截断。
		 
		 会申请新的内存空间，并将原来的内存空间的内容复制到新的内存空间中，然后释放原来的内存空间。
		 如果ptr为NULL，则相当于malloc函数；如果ptr不为NULL，则相当于free函数+malloc函数。
		    int main() {
			int*p=(int*)malloc(40);
			if (p == NULL) {
				printf("%s\n", strerror(errno)); //perror("自定义报错内容")
				return 1;
			}
			for (int i = 0; i < 10; i++) {
				*(p + i) = i;
			}

			//p = realloc(p, 80);//这样会比较危险，如果realloc失败，p会指向原来的地址，而不是新的地址，所以需要先将p的值保存到一个临时变量中。
			int* ptr = realloc(p, 80);
			if (ptr != NULL) {
				p = ptr;
			}
			for (int i; i < 10; i++) {
				printf("%d ", *(p + i));
			}
			free(p);
			p = NULL;
			return 0;
		}

	  内存池：专门用于管理动态内存分配的内存区域，可以提高内存分配的效率，减少内存碎片。
	
	  5.常见的动态内存错误：
	    5.1对NULL指针的解引用操作
		   void test()
			 {
					 int *p = (int *)malloc(INT_MAX/4);
					 *p = 20;//如果p的值是NULL，就会有问题
					free(p);
			 }
	    5.2对动态开辟空间的越界访问
		void test()
		 {
			 int i = 0;
			 int *p = (int *)malloc(10*sizeof(int));
			 if(NULL == p)
			 {
				 exit(EXIT_FAILURE);
			 }
			 for(i=0; i<=10; i++)
			 {
				 *(p+i) = i;//当i是10的时候越界访问
		   	}
			 free(p);
		 }
		 5.3 对非动态开辟内存使用free释放
		 void test()
		 {
			 int a = 10;
			 int *p = &a;
			 free(p);//ok?
		 }
		 5.4 使用free释放一块动态开辟内存的一部分
		 void test()
		 {
			 int *p = (int *)malloc(100);
			 p++;//可以换成*(p+i)或者p[i]
			 free(p);//p不再指向动态内存的起始位置
		}
		5.5对同一块动态内存多次释放
		 void test()
		 {
			 int *p = (int *)malloc(100);
			 free(p);
			 free(p);//重复释放
		}
		5.6动态开辟内存忘记释放（内存泄漏）
		void test()
		 {
			 int *p = (int *)malloc(100);
			 if(NULL != p)
			 {
				 *p = 20;
			 }
			 }
			 int main()
			 {
				 test();
				 while(1);
			  }
		 }
	   5.7 柔性数组
	   结构中的柔性数组成员前面必须至少一个其他成员。
	   sizeof 返回的这种结构大小不包括柔性数组的内存。
	   typedef struct st_type{
	       int i;
		   int a[]; //柔性数组成员，表示数组的大小不固定，可以根据需要动态分配内存
	   };
	   包含柔性数组成员的结构用malloc ()函数进行内存的动态分配，并且分配的内存应该大于结构的大小，以适应柔性数组的预期大小
	    使用：
		   struct *S=(struct*)malloc(sizeof(struct st_type)+40);//int i:sizeof(struct st_type)   ;               40:int a[]
	BIT-5 文件的操作
		1.文件的打开和关闭
		  文件指针：
			  即：文件类型指针，指向文件的首地址，文件指针是一个结构体类型的指针，包含了文件的相关信息，取名为FILE
		  定义file*变量：
			  file* pf; // 定义一个文件指针变量fp，指向文件的首地址
		  打开文件：
			   file* fopen(const char* filename, const char* mode*（文件的打开模式：读、写.....));  如果打开失败，返回NULL。
		  关闭文件：
			   int fclose(file* fp);  会刷新缓冲区
		  
		  int main ()
		 {
				FILE * pFile;
				//打开文件
				pFile = fopen ("myfile.txt","w");//w（写入/覆盖）,a（追加）,r（只读）、b（二进制文件）
				//文件操作
				if (pFile!=NULL)
				{
					fputs ("fopen example",pFile);
				//关闭文件
					fclose (pFile);
				}
				fclose (pFile);
				pFile=NULL;
				return 0;
		 }

		2.文件的顺序读写：
		一个字符的操作：如果操作失败，返回EOF
		字符输入函数-读文件	fgetc	所有输入流
		字符输出函数-写文件	fputc	所有输出流  

		一行的数据操作：如果操作失败，返回EOF  
		文本行输入函数-读文件	fgets	所有输入流读取的时候回自动添加'\n'结束符，如fgets(arr,5,pf);看似是读取5个字符，实际上是读取4个字符+1个'\n'结束符
		文本行输出函数-写文件	fputs	所有输出流  

		针对不同类型进行读写操作：
		格式化输入函数	fscanf	所有输入流
		格式化输出函数	fprintf	所有输出流

		任何一个C程序，只要运行起来就会默认打开一个标准输入流（stdin）、标准输出流（stdout）和标准错误流（stderr），这三个流都是文件指针类型的变量，分别指向标准输入、标准输出和标准错误输出。

		二进制的读写操作：fopen设置文件参数需存在b，但是b这个参数不能单独使用
		二进制输入		fread	文件
		二进制输出		fwrite	文件

		3.输入\输出函数的对比：
		printf、scanf：是标准输入输出函数，主要用于控制台的输入输出，格式化输出和输入。
		fprintf、fscanf：是对所有文件的输入输出函数，主要用于文件的输入输出，格式化输出和输入。
		
		针对字符串的输入输出函数（序列化/反序列化）：
		sprintf：sprintf(接受转化变量，"%d %ld %f",m)：把一个格式化的字符串输出到一个字符串中，返回值为输出的字符个数。
		sscanf： sscanf(接受转化变量，"%d %ld %f",m)：从一个字符串中获取一段格式化数据，返回值为成功读取的字段数。

		4.文件的随机读写：
		  1.fseek函数：根据偏移量和起始位置移动文件指针到指定位置，返回值为0表示成功，-1表示失败。
		    fseek(文件指针，偏移量，位置（SEEK_SET（起始位置）、SEEK_CUR（接着上一次的位置）、SEEK_END（文件末尾））)

		  2.ftell函数：获取文件指针当前位置的偏移量，返回值为当前文件指针的位置（字节数），如果失败，返回-1。
		    
		  3.rewind函数：将文件指针移动到文件的起始位置，返回值为0表示成功，-1表示失败。
			rewind(文件指针); //将文件指针移动到文件的起始位置

		5.文本文件与二进制文件
		  文本文件：以ASCII字符的形式存储数据，文件内容可以直接用文本编辑器打开查看，通常以.txt、.csv等格式保存。
		  二进制文件：以二进制形式存储数据，文件内容不能直接用文本编辑器打开查看，通常以.dat、.bin等格式保存。
		6.文件读取结束的判定
		  feof函数：用于判断文件指针是否到达文件末尾，返回值为0表示未到达末尾，非0表示已到达末尾,它本身不是来判断文件失败的原因

		  文本文件读取结束的判定通常有以下几种方式：判断返回值为EOF或者NULL
			fgetc：返回值为EOF表示读取结束
			fgets  返回值为NULL表示读取结束
		  二进制文件读取结束的判定通常有以下几种方式：判断返回值是否小于实际要读的个数
			fread：返回值小于实际要读的个数表示读取结束

		7.文件缓冲区：
		  缓冲区来处理数据文件，在每一个正在使用的文件开辟一块缓冲区，缓冲区的大小由系统决定，通常为4096字节或8192字节。
		  缓冲区的作用是提高文件的读写效率，减少磁盘的读写次数，缓冲区会在内存中保存一部分数据，当缓冲区满时，会将数据写入程序数据区。

	  BIT-7 程序环境和预处理
	    1.程序的翻译环境（将源代码转换为可执行的机器指令）和执行环境（执行相关代码）
		2.翻译环境：
		  每个源文件都会通过编译器，生产.obj的目标文件，目标文件是机器指令的二进制文件，包含了源代码的编译结果。
		  所有的目标文件会被链接器链接成一个.exe可执行文件
		  编译的阶段：预编译/预处理（启动对应的头文件、宏、定义和注释删除等文本操作，test.i）、
					  编译（把C语言转换为汇编代码（语法、词法、语义分析，符号汇总）,test.s）、
					  汇编（将汇编代码（test.s）转换成二进制指令，形成符号表（对之前的符号汇总进行融合为符号表），test.o）、
					  链接（合并段表、符号表的定位和重定位（更新主函数中的所引用的函数地址））  
		3.执行环境：
		  程序载入到内存中，操作系统会为程序分配一个虚拟地址空间，虚拟地址空间包括代码段、数据段、堆区和栈区。
		  程序执行便开始运行，找main函数
		  开始执行
		  终止程序
		4.预处理详解：
		  1.预定义符号：预处理器会定义一些预定义符号，这些预定义符号可以在程序中使用，预定义符号通常以__开头，例如__FILE__、__LINE__、__DATE__、__TIME__等。
		    __FILE__		//进行编译的源文件
			__LINE__		//文件当前的行号
			__DATE__		//文件被编译的日期
			__TIME__		//文件被编译的时间
			__STDC__		//如果编译器遵循ANSI C，其值为1，否则未定义
		 2.define定义标识符：
		   本质在编译下时，进行文本替换，预处理器会将所有的#define定义的标识符替换为对应的值。 
		 3.define定义宏：
		  参数只是进行替换文本，不参与计算，预处理器会将所有的宏函数调用替换为对应的宏函数体，所以在定义宏时，尽量不要使用复杂的表达式和不省（）。
		  不能递归，可以宏中使用define的参数
		   //#define Square(x) (x*x) //宏定义，定义一个宏函数Square(x)，用于计算x的平方
		   #define Square(x) ((x)*(x)) //宏定义，定义一个宏函数Square(x)，用于计算x的平方
		   int main() {
			   int a= Square(5); //调用宏函数Square(x)，计算5的平方：5*5=25
			   int b = Square(6 + 1); //调用宏函数Square(x)，计算8+1的平方：6+1*6+1=13
			   return 0;
			   }
		  
		    #和##
			#NAME：将NAME转换为字符串常量，预处理器会将所有的NAME替换为对应的字符串常量。：
			#define print(N,FORMAT) printf("this is a"#N，FORMAT“\n",N);
			int main() {
				int a = 10;
				print(a,%d); //输出：this is a 10
				float b = 3.14;
				print(b,%ld)
				return 0;
			}

			##NAME：将NAME转换为标识符，预处理器会将所有的NAME替换为对应的标识符。
			#define CONCAT(a, b) a##b //将a和b连接成一个标识符
			int main() {
				int ab = 10;
				printf("%d\n", CONCAT(a, b)); //输出：10
				return 0;
			}

			副作用的宏：
			#define MAX(a,b) ((a) > (b) ? (a) : (b)) //宏定义，计算两个数的最大值
			int main() {
			   int a=5;
			   int b=4;
			   printf("%d\n", MAX(++a, b++)); //输出：6
			   printf("%d\n", a); //输出：7
			   printf("%d\n", b); //输出：5

		  5.宏与函数：
			宏的规模和速度比函数快，但宏的缺点是没有类型检查，可能会导致一些意想不到的错误，不能调试。
			#undef：用于取消宏定义，预处理器会将所有的宏定义取消，所以在使用宏时，尽量不要使用复杂的表达式和不省（）。

		  6.include文件包含
			包含头文件的方式有两种：
			1.#include <文件名>：用于包含系统头文件，预处理器会在系统头文件目录中查找文件名对应的头文件。
			2.#include "文件名"：用于包含用户自定义头文件，预处理器会在当前目录中查找文件名对应的头文件，如果没有找到，则在系统头文件目录中查找。
			为了避免头文件重复包含，可以使用条件编译指令： #pragma once //只包含一次


	*/

